-
Notifications
You must be signed in to change notification settings - Fork 725
fix: do not double hash in secp256r1-verify
#6763
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
fix: do not double hash in secp256r1-verify
#6763
Conversation
This is a consensus breaking change and will need to go out in a hard-fork with a new version of Clarity (Clarity5). In the existing implementation, the code was incorrectly hashing the passed in `message-hash`. Unfortunately, the test code was also double-hashing on the signing side as well, so this was not caught. The bug was discovered by @eric-stacks when attempting to generate signatures externally (e.g. with WebAuthn) and verify them in Clarity code.
Codecov Report❌ Patch coverage is
❌ Your project check has failed because the head coverage (66.83%) is below the target coverage (80.00%). You can increase the head coverage or adjust the target coverage.
Additional details and impacted files@@ Coverage Diff @@
## develop #6763 +/- ##
============================================
- Coverage 78.18% 66.83% -11.35%
============================================
Files 580 585 +5
Lines 361096 361965 +869
============================================
- Hits 282312 241918 -40394
- Misses 78784 120047 +41263
... and 317 files with indirect coverage changes Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
|
I think there's some test vectors for secp256r1 produced by NIST -- see the test vectors section at the bottom here (https://csrc.nist.gov/Projects/cryptographic-algorithm-validation-program/Component-Testing): Not sure if these would have caught the fact that we're double hashing. |
|
Yeah -- I think we probably would have caught this if we used the NIST test vectors: diff --git a/clarity/src/vm/tests/crypto.rs b/clarity/src/vm/tests/crypto.rs
index 05d9460320..842090421e 100644
--- a/clarity/src/vm/tests/crypto.rs
+++ b/clarity/src/vm/tests/crypto.rs
@@ -3,13 +3,81 @@ use clarity_types::VmExecutionError;
use proptest::prelude::*;
use stacks_common::types::chainstate::{StacksPrivateKey, StacksPublicKey};
use stacks_common::types::{PrivateKey, StacksEpochId};
-use stacks_common::util::hash::{to_hex, Sha256Sum};
+use stacks_common::util::hash::{Sha256Sum, hex_bytes, to_hex};
use stacks_common::util::secp256k1::MessageSignature as Secp256k1Signature;
-use stacks_common::util::secp256r1::{Secp256r1PrivateKey, Secp256r1PublicKey};
+use stacks_common::util::secp256r1::{MessageSignature, Secp256r1PrivateKey, Secp256r1PublicKey};
use crate::vm::types::{ResponseData, TypeSignature, Value};
use crate::vm::{execute_with_parameters, ClarityVersion};
+struct NistVector {
+ msg: &'static str,
+ d: &'static str,
+ q_x: &'static str,
+ q_y: &'static str,
+ r: &'static str,
+ s: &'static str,
+}
+
+static NIST_VECTORS: &[NistVector] = &[
+ NistVector {
+ msg: "44acf6b7e36c1342c2c5897204fe09504e1e2efb1a900377dbc4e7a6a133ec56",
+ d: "519b423d715f8b581f4fa8ee59f4771a5b44c8130b4e3eacca54a56dda72b464",
+ q_x: "1ccbe91c075fc7f4f033bfa248db8fccd3565de94bbfb12f3c59ff46c271bf83",
+ q_y: "ce4014c68811f9a21a1fdb2c0e6113e06db7ca93b7404e78dc7ccd5ca89a4ca9",
+ r: "f3ac8061b514795b8843e3d6629527ed2afd6b1f6a555a7acabb5e6f79c8c2ac",
+ s: "8bf77819ca05a6b2786c76262bf7371cef97b218e96f175a3ccdda2acc058903",
+ }
+];
+
+#[rstest]
+#[case(0)]
+fn secp256r1_verify_valid_signatures_nist(
+ #[case] nist_index: usize
+) {
+ let NistVector { msg, d, q_x, q_y, r, s } = NIST_VECTORS[nist_index];
+
+ let message_hash = hex_bytes(msg).unwrap();
+ let privk = Secp256r1PrivateKey::from_hex(d).unwrap();
+ let mut pubk = Secp256r1PublicKey::from_hex(&format!("04{q_x}{q_y}")).unwrap();
+ pubk.set_compressed(true);
+
+ let signature_dh = privk.sign(&message_hash).unwrap();
+ let signature_nist = MessageSignature::from_hex(&format!("{r}{s}")).unwrap();
+
+ let verified_double_hash = pubk.verify(&message_hash, &signature_dh).is_ok();
+ let verified_nist_sign = pubk.verify_digest(&message_hash, &signature_nist).is_ok();
+
+ info!(
+ "Signed with SK";
+ "nist_signature" => format!("{r}{s}"),
+ "double_hash" => to_hex(&signature_dh.0),
+ "pubk" => to_hex(&pubk.to_bytes()),
+ "verified_double_hash" => verified_double_hash,
+ "verified_nist_sign" => verified_nist_sign,
+ );
+
+ let program = format!(
+ "(secp256r1-verify {} {} {})",
+ buff_literal(&message_hash),
+ buff_literal(&signature_nist.0),
+ buff_literal(&pubk.to_bytes())
+ );
+
+ assert_eq!(
+ Value::Bool(true),
+ execute_with_parameters(
+ program.as_str(),
+ ClarityVersion::Clarity4,
+ StacksEpochId::Epoch33,
+ false
+ )
+ .expect("execution should succeed")
+ .expect("should return a value")
+ );
+}
+
+
fn secp256r1_vectors() -> (Vec<u8>, Vec<u8>, Vec<u8>) {
let privk = Secp256r1PrivateKey::from_seed(&[7u8; 32]);
let pubk = Secp256r1PublicKey::from_private(&privk);That test fails: But you can see that it would have passed with pre-hash verification. |
|
Thanks for checking that Aaron. Want to just push that test to this branch? |
|
I added in them 8b8451a. |
|
Thxs @eric-stacks for discovering this. |
|
Related for your consideration @obycode https://github.com/Rapha-btc/csw-locker-contracts/blob/main/readme-webauthn.md also fyi summary: WebAuthn signature verification for Clarity 5+ |
jcnelson
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, just a few comments.
|
|
||
| /// Sign a message hash, returning the signature. | ||
| /// Sign a message hash, SHA256 hashing it (i.e. double-hashing) before | ||
| /// returning the signature. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We may want to explicitly note that the sign() and verify() functions have been (or will be) superseded by the sign_digest() and verify_digest() functions. Based on the code above, I don't think we intend to expose the old double-SHA256 behavior in Clarity 5, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, it will not be exposed in Clarity 5, but it needs to continue to exist for Clarity 4 contracts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a note in 282a71d.
282a71d
This is a consensus breaking change and will need to go out in a hard-fork with a new version of Clarity (Clarity5). In the existing implementation, the code was incorrectly hashing the passed in
message-hash. Unfortunately, the test code was also double-hashing on the signing side as well, so this was not caught. The bug was discovered by @eric-stacks when attempting to generate signatures externally (e.g. with WebAuthn) and verify them in Clarity code. The docs are updated to match the actual behavior and will need to be updated again once Clarity 5 is added.